Skip to content

ScalarUdfImpl::strictly_order_preserving: Allow expression to report whether they keep the same ordering of the input - #23807

Merged
rluvaton merged 11 commits into
apache:mainfrom
rluvaton:debug-why-sort-ordering-get-messed-up
Jul 27, 2026
Merged

ScalarUdfImpl::strictly_order_preserving: Allow expression to report whether they keep the same ordering of the input#23807
rluvaton merged 11 commits into
apache:mainfrom
rluvaton:debug-why-sort-ordering-get-messed-up

Conversation

@rluvaton

@rluvaton rluvaton commented Jul 22, 2026

Copy link
Copy Markdown
Member

Which issue does this PR close?

Related to:

Rationale for this change

To be able to keep the same sorting order allowing for more optimizations

Now comet own cast implementation can be recognized as not modifying sort order in the same cases that datafusion cast does.

What changes are included in this PR?

added strictly_order_preserving property to ExprProperties + varius other places
and replaced the hard coded logic for cast (substitute_cast_ordering) about keeping input order with more generic approach that now any expression can implement and have the same advantage of sort elimination

also marked from_unixtime as keeping ordering to show a case of this optimization

Are these changes tested?

yes

Are there any user-facing changes?

yes, breaking change, added strictly_order_preserving property to ExprProperties and to FFI_ExprProperties

this property means that given expression f and 2 values from the input column a and b the following variants are kept:

  1. a.cmp(b) == f(a).cmp(f(b))
  2. nulls maps to nulls

Example of satisfying expression:
cast(col_a as BIGINT) where col_a is INT it is keeping the properties

Example of not satisfying:

floor - floor can not

array_repeat(my_col, 2) which might look like at first glance as keeping the property as well but in fact it does not.

the reason is that array_repeat(null, 2) will output list of 2 nulls which breaks the 2nd property that nulls must be kept as nulls

how to migrate:

Option 1 - keeping the old behavior (safest but least performant)
set strictly_order_preserving to false

Option 2 - Using the new optimization that this opens up:
set strictly_order_preserving only if the expression keep both variants

⚠️ Also look for code that blindly copy child properties since it will copy the child strictly order preserving as well which will lead to wrong results

fn get_properties(&self, children: &[ExprProperties]) -> Result<ExprProperties> {
        // ⚠️ Clone here is BAD since you take the following 2 properties from your child that might not be true in your case:
        // - `preserves_lex_ordering`
        // - `strictly_order_preserving`
        // both of them should never be taken from the child as is since they talk about **this** expression 
        Ok(children[0].clone())
}

@github-actions github-actions Bot added logical-expr Logical plan and expressions physical-expr Changes to the physical-expr crates core Core DataFusion crate functions Changes to functions implementation ffi Changes to the ffi crate labels Jul 22, 2026
@rluvaton rluvaton changed the title Debug why sort ordering get messed up Allow expression to report whether they keep the same ordering of the input (a.cmp(b) == f(a).cmp(f(b)) opening up for more optimization Jul 22, 2026
@rluvaton
rluvaton force-pushed the debug-why-sort-ordering-get-messed-up branch from e0c40b1 to 7305547 Compare July 22, 2026 15:13
@rluvaton rluvaton added the api change Changes the API exposed to users of the crate label Jul 22, 2026
@rluvaton
rluvaton marked this pull request as ready for review July 22, 2026 15:19
@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown

Thank you for opening this pull request!

Reviewer note: cargo-semver-checks reported the current version number is not SemVer-compatible with the changes in this pull request (compared against the base branch).

Details
     Cloning apache/main
    Building datafusion v54.1.0 (current)
       Built [ 110.793s] (current)
     Parsing datafusion v54.1.0 (current)
      Parsed [   0.037s] (current)
    Building datafusion v54.1.0 (baseline)
       Built [ 109.080s] (baseline)
     Parsing datafusion v54.1.0 (baseline)
      Parsed [   0.038s] (baseline)
    Checking datafusion v54.1.0 -> v54.1.0 (no change; assume patch)
     Checked [   0.907s] 223 checks: 223 pass, 30 skip
     Summary no semver update required
    Finished [ 222.448s] datafusion
    Building datafusion-expr v54.1.0 (current)
       Built [  26.512s] (current)
     Parsing datafusion-expr v54.1.0 (current)
      Parsed [   0.081s] (current)
    Building datafusion-expr v54.1.0 (baseline)
       Built [  26.338s] (baseline)
     Parsing datafusion-expr v54.1.0 (baseline)
      Parsed [   0.082s] (baseline)
    Checking datafusion-expr v54.1.0 -> v54.1.0 (no change; assume patch)
     Checked [   1.863s] 223 checks: 223 pass, 30 skip
     Summary no semver update required
    Finished [  56.283s] datafusion-expr
    Building datafusion-expr-common v54.1.0 (current)
       Built [  18.621s] (current)
     Parsing datafusion-expr-common v54.1.0 (current)
      Parsed [   0.020s] (current)
    Building datafusion-expr-common v54.1.0 (baseline)
       Built [  18.459s] (baseline)
     Parsing datafusion-expr-common v54.1.0 (baseline)
      Parsed [   0.021s] (baseline)
    Checking datafusion-expr-common v54.1.0 -> v54.1.0 (no change; assume patch)
     Checked [   0.282s] 223 checks: 222 pass, 1 fail, 0 warn, 30 skip

--- failure constructible_struct_adds_field: externally-constructible struct adds field ---

Description:
A pub struct constructible with a struct literal has a new pub field. Existing struct literals must be updated to include the new field.
        ref: https://doc.rust-lang.org/reference/expressions/struct-expr.html
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.49.0/src/lints/constructible_struct_adds_field.ron

Failed in:
  field ExprProperties.strictly_order_preserving in /home/runner/work/datafusion/datafusion/datafusion/expr-common/src/sort_properties.rs:198

     Summary semver requires new major version: 1 major and 0 minor checks failed
    Finished [  38.131s] datafusion-expr-common
    Building datafusion-ffi v54.1.0 (current)
       Built [  59.438s] (current)
     Parsing datafusion-ffi v54.1.0 (current)
      Parsed [   0.065s] (current)
    Building datafusion-ffi v54.1.0 (baseline)
       Built [  60.755s] (baseline)
     Parsing datafusion-ffi v54.1.0 (baseline)
      Parsed [   0.068s] (baseline)
    Checking datafusion-ffi v54.1.0 -> v54.1.0 (no change; assume patch)
     Checked [   0.330s] 223 checks: 223 pass, 30 skip
     Summary no semver update required
    Finished [ 122.024s] datafusion-ffi
    Building datafusion-functions v54.1.0 (current)
       Built [  30.721s] (current)
     Parsing datafusion-functions v54.1.0 (current)
      Parsed [   0.092s] (current)
    Building datafusion-functions v54.1.0 (baseline)
       Built [  31.054s] (baseline)
     Parsing datafusion-functions v54.1.0 (baseline)
      Parsed [   0.095s] (baseline)
    Checking datafusion-functions v54.1.0 -> v54.1.0 (no change; assume patch)
     Checked [   0.619s] 223 checks: 223 pass, 30 skip
     Summary no semver update required
    Finished [  63.807s] datafusion-functions
    Building datafusion-physical-expr v54.1.0 (current)
       Built [  29.108s] (current)
     Parsing datafusion-physical-expr v54.1.0 (current)
      Parsed [   0.053s] (current)
    Building datafusion-physical-expr v54.1.0 (baseline)
       Built [  29.378s] (baseline)
     Parsing datafusion-physical-expr v54.1.0 (baseline)
      Parsed [   0.054s] (baseline)
    Checking datafusion-physical-expr v54.1.0 -> v54.1.0 (no change; assume patch)
     Checked [   0.486s] 223 checks: 223 pass, 30 skip
     Summary no semver update required
    Finished [  59.929s] datafusion-physical-expr
    Building datafusion-sqllogictest v54.1.0 (current)
       Built [ 191.897s] (current)
     Parsing datafusion-sqllogictest v54.1.0 (current)
      Parsed [   0.023s] (current)
    Building datafusion-sqllogictest v54.1.0 (baseline)
       Built [ 185.890s] (baseline)
     Parsing datafusion-sqllogictest v54.1.0 (baseline)
      Parsed [   0.024s] (baseline)
    Checking datafusion-sqllogictest v54.1.0 -> v54.1.0 (no change; assume patch)
     Checked [   0.120s] 223 checks: 223 pass, 30 skip
     Summary no semver update required
    Finished [ 380.933s] datafusion-sqllogictest

@github-actions github-actions Bot added the auto detected api change Auto detected API change label Jul 22, 2026
@rluvaton

Copy link
Copy Markdown
Member Author

Cc @andygrove, @mbutrovich, @ozankabak

@rluvaton rluvaton changed the title Allow expression to report whether they keep the same ordering of the input (a.cmp(b) == f(a).cmp(f(b)) opening up for more optimization Allow expression to report whether they keep the same ordering of the input (a.cmp(b) == f(a).cmp(f(b))) opening up for more optimization Jul 22, 2026
if is_order_preserving_cast_family(&child.range.data_type(), target_type) {
Ok(child.clone().with_range(unbounded))
let source_type = child.range.data_type();
// A widening cast is additionally one-to-one, so it is strictly

@getChan getChan Jul 22, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I may not have full context, so please feel free to ignore this if it's a misunderstanding.

CastExpr::check_bigger_cast includes Int32 -> Float32 and Int64 -> Float64. Due to float mantissa limits, large integers lose precision and can collapse to the same float value (e.g. 16_777_216_i32 as f32 == 16_777_217_i32 as f32), so they are not strictly 1-to-1

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, unfortunately this moved from here (so no logic change has been made):

&& CastExpr::check_bigger_cast(cast_expr.cast_type(), expr_type))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

filed. #23808

preserves_lex_ordering: value.preserves_lex_ordering,
// Not carried over the FFI boundary (it would break the ABI);
// stay conservative.
strictly_order_preserving: false,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should I make breaking change in FFI (by adding the field) while I'm here to avoid 2 breaking changes? I'm not really familiar with this module

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've read .ai/skills/datafusion-ffi/SKILL.md (ironic) and saw that it is ok to make breaking change, just not release them in a patch version

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so I added the field here

if is_order_preserving_cast_family(&child.range.data_type(), target_type) {
Ok(child.clone().with_range(unbounded))
let source_type = child.range.data_type();
// A widening cast is additionally one-to-one, so it is strictly

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, unfortunately this moved from here (so no logic change has been made):

&& CastExpr::check_bigger_cast(cast_expr.cast_type(), expr_type))

@codecov-commenter

codecov-commenter commented Jul 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.59794% with 13 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.66%. Comparing base (7576762) to head (ad7d27b).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
...tafusion/physical-expr/src/equivalence/ordering.rs 72.00% 0 Missing and 7 partials ⚠️
...on/physical-expr/src/equivalence/properties/mod.rs 84.84% 0 Missing and 5 partials ⚠️
datafusion/physical-expr/src/scalar_function.rs 50.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #23807      +/-   ##
==========================================
- Coverage   80.66%   80.66%   -0.01%     
==========================================
  Files        1094     1095       +1     
  Lines      371671   372376     +705     
  Branches   371671   372376     +705     
==========================================
+ Hits       299817   300380     +563     
- Misses      53958    54067     +109     
- Partials    17896    17929      +33     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@mbutrovich
mbutrovich self-requested a review July 24, 2026 16:29

@alamb alamb left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @rluvaton -- this idea here seems sound to me

Can we get some sql logic / explain test that show some of the optimizations that this allows for (that preserves_lex_ordering does not)?

I think having those examples would make sure we have a good / minimal API to build on -- without such "end visible facing" tests I fear we may implement some features that are not ever used

Ok(false)
}

/// Returns true if the function is strictly order-preserving with respect

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is this related to the output_ordering method? I am wondering if we can extend that rather tan introduce a new method

https://docs.rs/datafusion/latest/datafusion/logical_expr/trait.ScalarUDFImpl.html#method.output_ordering

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

output_ordering does not tell you whether you can reuse the existing ordering when you have multiple order by keys (with how ties are handled)

Comment thread datafusion/expr/src/udf.rs Outdated
///
/// This is not simply a stricter [`Self::preserves_lex_ordering`] — the
/// two properties also assume different input orderings, and with
/// multiple ordered inputs neither implies the other. Monotone but

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would have helped me to move these examples up to the top of the docs

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved

preserves_lex_ordering: value.preserves_lex_ordering,
// Not carried over the FFI boundary (it would break the ABI);
// stay conservative.
strictly_order_preserving: false,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rluvaton

Copy link
Copy Markdown
Member Author

Thank you @rluvaton -- this idea here seems sound to me

Can we get some sql logic / explain test that show some of the optimizations that this allows for (that preserves_lex_ordering does not)?

I think having those examples would make sure we have a good / minimal API to build on -- without such "end visible facing" tests I fear we may implement some features that are not ever used

I did not want to add uses of this api in this PR to reduce the amount of code to review, do you still want me to do so?

@rluvaton

rluvaton commented Jul 25, 2026

Copy link
Copy Markdown
Member Author

Can we get some sql logic / explain test that show some of the optimizations that this allows for (that preserves_lex_ordering does not)?

Done, array_repeat is now no longer remove the ordering
actually array_repeat does not map null to null, changed to from_unixtime

…d-ordering

# Conflicts:
#	datafusion/expr-common/src/sort_properties.rs
#	datafusion/expr/src/udf.rs
@github-actions github-actions Bot added the sqllogictest SQL Logic Tests (.slt) label Jul 25, 2026
@rluvaton

Copy link
Copy Markdown
Member Author

@alamb can you please take another look?

rluvaton pushed a commit to rluvaton/datafusion that referenced this pull request Jul 26, 2026
…xpr::check_bigger_cast (apache#23808) (apache#23809)

## Which issue does this PR close?

- Closes apache#23808.

## Rationale for this change
 ref. apache#23807

`CastExpr::check_bigger_cast` is used to determine whether a cast is a
widening (order-preserving) conversion. Currently, it classifies `Int32
-> Float32`, `UInt32 -> Float32`, `Int64 -> Float64`, and `UInt64 ->
Float64` as widening casts.

However, integer-to-float conversions for 32-bit and 64-bit integers
lose precision when values exceed the mantissa bit limit (24 bits for
`Float32`, 53 bits for `Float64`). For example:
`16_777_216_i32 as f32 == 16_777_217_i32 as f32` (both yield
16777216.0f32).

Because distinct integer inputs can collapse to the same float output,
these casts are not strictly 1-to-1 (injective) and can break suffix
sort key ordering in multi-column ordering analysis (e.g. `[CAST(a AS
Float32), b]`).

## What changes are included in this PR?

- Updated `CastExpr::check_bigger_cast` to exclude precision-losing
integer-to-float conversions (`Int32/UInt32 -> Float32` and
`Int64/UInt64 -> Float64`).
- Added unit test `test_check_bigger_cast_precision_loss` to verify
precision-losing casts return `false` while exact conversions (`Int16 ->
Float32`, `Int32 -> Float64`, etc.) continue to return `true`.

## Are these changes tested?

Yes, new unit test `test_check_bigger_cast_precision_loss` in `cast.rs`.

## Are there any user-facing changes?

No breaking API changes. Internal optimizer behavior fix.

@alamb alamb left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @rluvaton - this looks good to me

I suggest we file a ticket / tickets about marking other functions as strictly_order_preserving so that we don't forget / so we can take advantage

Comment thread datafusion/expr-common/src/sort_properties.rs
Comment thread datafusion/expr-common/src/sort_properties.rs

/// Returns true if the function preserves lexicographical ordering based on
/// the input ordering.
///

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be ok to keep these descriptions short and refer readers to the very nice SortProperties description

Eg. something like

Suggested change
///
/// See [`ExprProperties::preserves_lex_ordering`] for more details

Comment on lines +998 to +1037
///
/// # Examples
///
/// * `from_unixtime(a)` returns `true`: it reinterprets the input integer
/// as a timestamp without changing the value, so distinct inputs yield
/// distinct, identically-ordered outputs.
/// * `floor(a)` and `date_trunc('day', a)` must return `false`: they are
/// monotone, but collapse distinct inputs into equal outputs.
/// * An addition over ordered, overflow-free inputs may return `true`
/// even though it does not preserve *lexicographical* ordering.
///
/// # Definition
///
/// Assuming the `Ordered` inputs advance simultaneously (component-wise,
/// i.e. all of them are sorted in the data), the output is ordered in the
/// same direction, equal outputs can only result from equal values of
/// those inputs (i.e. the mapping is one-to-one), and nulls map to nulls.
///
/// This is not simply a stricter [`Self::preserves_lex_ordering`] - the
/// two properties also assume different input orderings, and with
/// multiple ordered inputs neither implies the other (see the examples
/// above). See [`ExprProperties::strictly_order_preserving`] for a
/// detailed comparison.
///
/// # Relationship to [`Self::output_ordering`]
///
/// [`Self::output_ordering`] describes *whether and in which direction*
/// the output is ordered, which justifies using the expression as the
/// **last** (or only) sort key. This property is an additional,
/// independent claim of injectivity that justifies keeping the **suffix**
/// sort keys as well: optimizers rely on it to substitute a sort key with
/// an expression computed from it - if data is sorted by `[x, y]`, it is
/// also sorted by `[expr(x), y]`, which only holds when equal `expr(x)`
/// values imply equal `x` values.
///
/// When in doubt, return `false` (the default). The `inputs` properties
/// allow conditional answers, e.g. based on the ranges of the inputs.
fn strictly_order_preserving(&self, _inputs: &[ExprProperties]) -> Result<bool> {
Ok(false)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly I think here it would be ok to point back to ExprProperties to make sure the text stays consistent

Suggested change
///
/// # Examples
///
/// * `from_unixtime(a)` returns `true`: it reinterprets the input integer
/// as a timestamp without changing the value, so distinct inputs yield
/// distinct, identically-ordered outputs.
/// * `floor(a)` and `date_trunc('day', a)` must return `false`: they are
/// monotone, but collapse distinct inputs into equal outputs.
/// * An addition over ordered, overflow-free inputs may return `true`
/// even though it does not preserve *lexicographical* ordering.
///
/// # Definition
///
/// Assuming the `Ordered` inputs advance simultaneously (component-wise,
/// i.e. all of them are sorted in the data), the output is ordered in the
/// same direction, equal outputs can only result from equal values of
/// those inputs (i.e. the mapping is one-to-one), and nulls map to nulls.
///
/// This is not simply a stricter [`Self::preserves_lex_ordering`] - the
/// two properties also assume different input orderings, and with
/// multiple ordered inputs neither implies the other (see the examples
/// above). See [`ExprProperties::strictly_order_preserving`] for a
/// detailed comparison.
///
/// # Relationship to [`Self::output_ordering`]
///
/// [`Self::output_ordering`] describes *whether and in which direction*
/// the output is ordered, which justifies using the expression as the
/// **last** (or only) sort key. This property is an additional,
/// independent claim of injectivity that justifies keeping the **suffix**
/// sort keys as well: optimizers rely on it to substitute a sort key with
/// an expression computed from it - if data is sorted by `[x, y]`, it is
/// also sorted by `[expr(x), y]`, which only holds when equal `expr(x)`
/// values imply equal `x` values.
///
/// When in doubt, return `false` (the default). The `inputs` properties
/// allow conditional answers, e.g. based on the ranges of the inputs.
fn strictly_order_preserving(&self, _inputs: &[ExprProperties]) -> Result<bool> {
Ok(false)
}
///
/// See [`ExprProperties::strictly_order_preserving`] for more details
fn strictly_order_preserving(&self, _inputs: &[ExprProperties]) -> Result<bool> {
Ok(false)
}

}

fn strictly_order_preserving(&self, _inputs: &[ExprProperties]) -> Result<bool> {
// `from_unixtime` stores the input's exact `Int64` value as a

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

let col_b = col("b", &schema)?;
let asc = SortOptions::default();
let ordering = vec![
PhysicalSortExpr::new(Arc::clone(&col_a), asc),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could probably reduce the text here (and make it easier to read) maybe by creating

let ordering_a_b =  PhysicalSortExpr::new(..., asc),

?

Arc::clone(&narrowing),
asc
)])?);
assert!(!eq_properties.ordering_satisfy(vec![

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does still satisfy col_a I think -- it might be nice to add an assert here that it does satisfy

@alamb alamb changed the title Allow expression to report whether they keep the same ordering of the input (a.cmp(b) == f(a).cmp(f(b))) opening up for more optimization ScalarUdfImpl::strictly_order_preserving: Allow expression to report whether they keep the same ordering of the input Jul 27, 2026
@rluvaton
rluvaton added this pull request to the merge queue Jul 27, 2026
Merged via the queue into apache:main with commit 1c3232c Jul 27, 2026
41 checks passed
@rluvaton
rluvaton deleted the debug-why-sort-ordering-get-messed-up branch July 27, 2026 13:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api change Changes the API exposed to users of the crate auto detected api change Auto detected API change core Core DataFusion crate ffi Changes to the ffi crate functions Changes to functions implementation logical-expr Logical plan and expressions physical-expr Changes to the physical-expr crates sqllogictest SQL Logic Tests (.slt)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ProjectExec does not keep sorting information for passthrough expression

4 participants